﻿using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApplicationParts;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Serialization;
using PMS.Core.Configuration;
using PMS.Core.Infrastructure;
using PMS.Core.Redis;
using PMS.Core.Utils;
using PMS.Core.Utils.Authentication;
using PMS.Core.Utils.Http;
using PMS.Core.Utils.Security;
using PMS.Data.Entities.Settings;
using PMS.Services.Authentication.Externel;
using PMS.Services.SharedKernel;
using PMS.Services.Utils.BaiduAPI;
using PMS.Web.Framework.Mvc.ModelBinding;
using PMS.Web.Framework.Mvc.Routing;
using PMS.Web.Framework.StartupCfgs.StartupSetups;
using StackExchange.Profiling.Storage;
using System;
using System.Linq;
using System.Net;
using System.Text;
using WebMarkupMin.AspNetCore3;
using WebMarkupMin.Core;
using WebMarkupMin.NUglify;

namespace PMS.Web.Framework.StartupCfg.Extensions
{
    /// <summary>
    /// Represents extensions of IServiceCollection
    /// </summary>
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// Add services to the application and configure service provider
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        /// <param name="configuration">Configuration of the application</param>
        /// <param name="webHostEnvironment">Hosting environment</param>
        /// <returns>Configured service provider</returns>
        public static (IEngine, PMSConfig) ConfigureApplicationServices(this IServiceCollection services,
            IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
        {
            //most of API providers require TLS 1.2 nowadays
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            //add PMSConfig configuration parameters
            var pmsConfig = services.ConfigureStartupConfig<PMSConfig>(configuration.GetSection("PMS"));

            //add hosting configuration parameters
            services.ConfigureStartupConfig<HostingConfig>(configuration.GetSection("Hosting"));

            //add accessor to HttpContext
            services.AddHttpContextAccessor();

            //create default file provider
            CommonHelper.DefaultFileProvider = new PMSFileProvider(webHostEnvironment);

            //initialize plugins
            var mvcCoreBuilder = services.AddMvcCore();
            mvcCoreBuilder.PartManager.InitializePlugins(pmsConfig);

            //create engine and configure service provider
            var engine = EngineContext.Create();

            engine.ConfigureServices(services, configuration, pmsConfig);

            return (engine, pmsConfig);
        }

        /// <summary>
        /// Create, bind and register as service the specified configuration parameters 
        /// </summary>
        /// <typeparam name="TConfig">Configuration parameters</typeparam>
        /// <param name="services">Collection of service descriptors</param>
        /// <param name="configuration">Set of key/value application configuration properties</param>
        /// <returns>Instance of configuration parameters</returns>
        public static TConfig ConfigureStartupConfig<TConfig>(this IServiceCollection services, IConfiguration configuration) where TConfig : class, new()
        {
            if (services == null)
                throw new ArgumentNullException(nameof(services));

            if (configuration == null)
                throw new ArgumentNullException(nameof(configuration));

            //create instance of config
            var config = new TConfig();

            //bind it to the appropriate section of configuration
            configuration.Bind(config);

            //and register it as a service
            services.AddSingleton(config);

            return config;
        }

        /// <summary>
        /// Register HttpContextAccessor
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddHttpContextAccessor(this IServiceCollection services)
        {
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        }

        /// <summary>
        /// Adds services required for anti-forgery support
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddAntiForgery(this IServiceCollection services)
        {
            //override cookie name
            services.AddAntiforgery(options =>
            {
                options.Cookie.Name = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.AntiforgeryCookie}";

                //whether to allow the use of anti-forgery cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<IStoreContext>().CurrentStore.SslEnabled
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });
        }

        /// <summary>
        /// Adds services required for application session state
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddHttpSession(this IServiceCollection services)
        {
            services.AddSession(options =>
            {
                options.Cookie.Name = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.SessionCookie}";
                //DavidLee HttpOnly: true: 客户端JS对Cookie内容不可读
                //                   false:客户端JS对Cookie内容可读
                options.Cookie.HttpOnly = true;

                //whether to allow the use of session values from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<IStoreContext>().CurrentStore.SslEnabled
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });
        }

        /// <summary>
        /// Adds data protection services
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSDataProtection(this IServiceCollection services)
        {
            //check whether to persist data protection in Redis
            var pmsConfig = services.BuildServiceProvider().GetRequiredService<PMSConfig>();
            if (pmsConfig.RedisEnabled && pmsConfig.UseRedisToStoreDataProtectionKeys)
            {
                //store keys in Redis
                services.AddDataProtection().PersistKeysToStackExchangeRedis(() =>
                {
                    //For some reason, data protection services are registered earlier. This configuration is called even before the request queue starts. 
                    //Service provider has not yet been built and we cannot get the required service. 
                    //So we create a new instance of RedisConnectionWrapper() bypassing the DI.
                    var redisConnectionWrapper = new RedisConnectionWrapper(pmsConfig);
                    return redisConnectionWrapper.GetDatabase(pmsConfig.RedisDatabaseId ?? (int)RedisDatabaseNumber.DataProtectionKeys);
                }, DataProtectionDefaultConfigs.RedisDataProtectionKey);
            }
            else 
            {
                var dataProtectionKeysPath = CommonHelper.DefaultFileProvider.MapPath(DataProtectionDefaultConfigs.DataProtectionKeysPath);
                var dataProtectionKeysFolder = new System.IO.DirectoryInfo(dataProtectionKeysPath);

                //configure the data protection system to persist keys to the specified directory
                services.AddDataProtection().PersistKeysToFileSystem(dataProtectionKeysFolder);
            }
        }

        /// <summary>
        /// Adds authentication service
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSAuthentication(this IServiceCollection services)
        {
            //set default authentication schemes
            //DavidLee 由于程序启动加载时还未进行Autofac DI组件注册，无法获取CustomerSettings.AccountAuthenticationType
            //判断启用的登录方式为Cookie还是Jwt
            //故在切换Cookie/Jwt登录方式时，需要在此手动调整对应登录方式所使用的机制
            var authenticationBuilder = services.AddAuthentication(options =>
            {
                options.DefaultChallengeScheme = AuthenticationDefaultConfigs.JwtAuthenticationScheme;
                options.DefaultScheme = AuthenticationDefaultConfigs.JwtAuthenticationScheme;
                options.DefaultSignInScheme = AuthenticationDefaultConfigs.JwtAuthenticationScheme;
            });
           
            //add main cookie authentication
            authenticationBuilder.AddCookie(AuthenticationDefaultConfigs.DefaultAuthenticationScheme, options =>
            {
                options.Cookie.Name = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.AuthenticationCookie}";
                options.Cookie.HttpOnly = true;
                options.LoginPath = DefaultRoutePath.LoginPath;
                options.AccessDeniedPath = DefaultRoutePath.AccessDenied403;

                //whether to allow the use of authentication cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<IStoreContext>().CurrentStore.SslEnabled
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });

            //add external authentication
            authenticationBuilder.AddCookie(AuthenticationDefaultConfigs.ExternalAuthenticationScheme, options =>
            {
                options.Cookie.Name = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.ExternalAuthenticationCookie}";
                options.Cookie.HttpOnly = true;
                options.LoginPath = DefaultRoutePath.LoginPath;
                options.AccessDeniedPath = DefaultRoutePath.AccessDenied403;

                //whether to allow the use of authentication cookies from SSL protected page on the other store pages which are not
                options.Cookie.SecurePolicy = EngineContext.Current.Resolve<IStoreContext>().CurrentStore.SslEnabled
                    ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
            });
            
            //register and configure external authentication plugins now
            var typeFinder = new WebAppTypeFinder();
            var externalAuthConfigurations = typeFinder.FindClassesOfType<IExternalAuthenticationRegistrar>();
            var externalAuthInstances = externalAuthConfigurations
                .Select(x => (IExternalAuthenticationRegistrar)Activator.CreateInstance(x));

            foreach (var instance in externalAuthInstances)
                instance.Configure(authenticationBuilder);
        }

        /// <summary>
        /// Add and configure MVC for the application
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        /// <returns>A builder for configuring MVC services</returns>
        public static IMvcBuilder AddPMSMvc(this IServiceCollection services)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder => builder.WithOrigins(
                    new[] {
                        "http://localhost:59505",
                        "http://localhost:8080",
                        "http://localhost:80",
                    })
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .SetIsOriginAllowed((host) => true));
            });
            // 添加Signalr
            services.AddSignalR(config => {
                config.EnableDetailedErrors = true;
            });
            // Mvc Routing config: url route in lowercase for readable and system limits(e.g: linux system)
            // and SEO 
            services.AddRouting(options => {
                options.LowercaseUrls = true;
                //add constraint key for language
                options.ConstraintMap["lang"] = typeof(LanguageParameterTransformer);
            });

            //add basic MVC feature
            //https://stackoverflow.com/questions/36747293/how-to-specify-the-view-location-in-asp-net-core-mvc-when-using-custom-locations
            var mvcBuilder = services.AddControllersWithViews().AddRazorOptions(
                options => {// Add custom location to view search location
                    options.ViewLocationFormats.Add("/Views/Frontend/{1}/{0}.cshtml");
                    options.ViewLocationFormats.Add("/Views/Backend/{1}/{0}.cshtml");
            });

            mvcBuilder.AddRazorRuntimeCompilation();

            var pmsConfig = services.BuildServiceProvider().GetRequiredService<PMSConfig>();
            if (pmsConfig.UseSessionStateTempDataProvider)
            {
                //use session-based temp data provider
                mvcBuilder.AddSessionStateTempDataProvider();
            }
            else
            {
                //use cookie-based temp data provider
                mvcBuilder.AddCookieTempDataProvider(options =>
                {
                    options.Cookie.Name = $"{CookieDefaultConfigs.Prefix}{CookieDefaultConfigs.TempDataCookie}";

                    //whether to allow the use of cookies from SSL protected page on the other store pages which are not
                    options.Cookie.SecurePolicy = EngineContext.Current.Resolve<IStoreContext>().CurrentStore.SslEnabled
                        ? CookieSecurePolicy.SameAsRequest : CookieSecurePolicy.None;
                });
            }

            services.AddRazorPages();
            //sets the default value of settings on MvcOptions to match the behavior of asp.net core mvc 3.0
            mvcBuilder.SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
            //MVC now serializes JSON with camel case names by default, use this code to avoid it
            mvcBuilder.AddNewtonsoftJson(options =>
            {
                options.SerializerSettings.ContractResolver = new DefaultContractResolver();
                //options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                options.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss";

            });

            // DavidLee 2019/11/16 
            // 参考nopCommerce框架并改造满足前端ajax post至后端action方法的数据-参数模型绑定
            //add custom model binder provider (to the top of the provider list)
            mvcBuilder.AddMvcOptions(options =>
            {
                IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
                options.ModelBinderProviders.Insert(0, new EntityModelBinderProvider(options.InputFormatters, readerFactory));
            });

            //add fluent validation
            mvcBuilder.AddFluentValidation(configuration =>
            {
                //register all available validators from PMS assemblies
                //批量注册的方法 RegisterValidatorsFromAssemblies
                //通过这个方法可以注册特定程序集中的所有的验证器。这将自动查找从其继承 AbstractValidator 并在容器中注册的所有公共非抽象类型
                var assemblies = mvcBuilder.PartManager.ApplicationParts
                    .OfType<AssemblyPart>()
                    .Where(part => part.Name.StartsWith("PMS", StringComparison.InvariantCultureIgnoreCase))
                    .Select(part => part.Assembly);
                configuration.RegisterValidatorsFromAssemblies(assemblies);

                //implicit/automatic validation of child properties
                configuration.ImplicitlyValidateChildProperties = true;
            });

            //register controllers as services, it'll allow to override them
            mvcBuilder.AddControllersAsServices();

            return mvcBuilder;
        }

        /// <summary>
        /// Register custom RedirectResultExecutor
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSRedirectResultExecutor(this IServiceCollection services)
        {
            //we use custom redirect executor as a workaround to allow using non-ASCII characters in redirect URLs
            services.AddSingleton<IActionResultExecutor<RedirectResult>, PMSRedirectResultExecutor>();
        }

        /// <summary>
        /// Add and configure MiniProfiler service
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSMiniProfiler(this IServiceCollection services)
        {
            services.AddMiniProfiler(miniProfilerOptions =>
            {
                //use memory cache provider for storing each result
                ((MemoryCacheStorage)miniProfilerOptions.Storage).CacheDuration = TimeSpan.FromMinutes(60);

                //whether MiniProfiler should be displayed
                miniProfilerOptions.ShouldProfile = request =>
                    EngineContext.Current.Resolve<CommonSettings>().DisplayMiniProfiler;
            });
        }

        /// <summary>
        /// Add and configure WebMarkupMin service
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSWebMarkupMin(this IServiceCollection services)
        {
            services
                .AddWebMarkupMin(options =>
                {
                    options.AllowMinificationInDevelopmentEnvironment = true;
                    options.AllowCompressionInDevelopmentEnvironment = true;
                    options.DisableMinification = !EngineContext.Current.Resolve<CommonSettings>().EnableHtmlMinification;
                    options.DisableCompression = true;
                    options.DisablePoweredByHttpHeaders = true;
                    options.DefaultEncoding = Encoding.UTF8;
                })
                .AddHtmlMinification(options =>
                {//Html压缩
                    HtmlMinificationSettings settings = options.MinificationSettings;
                    settings.RemoveRedundantAttributes = true;
                    settings.RemoveHttpProtocolFromAttributes = true;
                    settings.RemoveHttpsProtocolFromAttributes = true;
                    settings.RemoveTagsWithoutContent = true;
                    settings.RemoveEmptyAttributes = true;
                    settings.RemoveHtmlComments = true;
                    settings.RemoveHtmlCommentsFromScriptsAndStyles = true;
                    settings.RemoveJsProtocolFromAttributes = true;
                    settings.MinifyEmbeddedJsCode = true;
                    settings.MinifyEmbeddedCssCode = true;
                    settings.MinifyEmbeddedJsonData = false;
                    settings.MinifyInlineCssCode = true;//页面内css压缩
                    settings.MinifyInlineJsCode = true;//页面内js压缩
                    options.CssMinifierFactory = new NUglifyCssMinifierFactory();
                    options.JsMinifierFactory = new NUglifyJsMinifierFactory();
                })
                .AddXmlMinification(options =>
                {
                    var settings = options.MinificationSettings;
                    settings.RenderEmptyTagsWithSpace = true;
                    settings.CollapseTagsWithoutContent = true;
                });
        }

        /// <summary>
        /// Add and configure default HTTP clients
        /// DavidLee: 根据实际情况不使用HttpClient Proxy,如需要使用再添加
        /// </summary>
        /// <param name="services">Collection of service descriptors</param>
        public static void AddPMSHttpClients(this IServiceCollection services)
        {
            //default client
            services.AddHttpClient(HttpDefaultConfigs.DefaultHttpClient);

            //client to request current store
            //services.AddHttpClient<StoreHttpClient>();

            //client to request BaiduFanyi request
            services.AddHttpClient<BaiduWebApiHttpClient>();

            //client to request reCAPTCHA service
            //services.AddHttpClient<CaptchaHttpClient>().WithProxy();
        }
    }
}